﻿//#define VERBOSE

using System;
using System.Diagnostics;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Entity.Scene;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Types;
using RayDen.RayEngine.Data;

namespace RayDen.RayEngine.Engines.PathTracer.MIS
{
    // ReSharper disable RedundantThisQualifier
    
    public class MISPathTracerPath : PathSamplerBase
    {
        internal RgbSpectrum Throughput;
        public PathTracerPathState PathState;
        private int RejectionSamplingMaxBsdfTries = 1;
        private int RejectionSamplingMaxLighTries = 1;

        protected int depth, tracedShadowRayCount;
        protected float pathWeight;
        protected float lastPdfW;
        protected bool specularBounce;

        protected SurfaceIntersectionData hitInfo;

        public UInt64 PathExpression;

        protected RayEngineScene scene;

        public int MaxRaysPerPath
        {
            get;
            set;
        }

        internal struct ShadowRayInfo
        {
            public float pdf;
            public float g;
            public RgbSpectrumInfo color;
            public RayData shadowRay;
            public int currentShadowRayIndex;


        }
        private ShadowRayInfo[] secRays;
        private LightSample ls;

        float Mis(float aPdf)
        {
            return aPdf;
        }

        // Mis weight for 2 pdfs
        float Mis2(
            float aSamplePdf,
            float aOtherPdf)
        {
            return Mis(aSamplePdf) / (Mis(aSamplePdf) + Mis(aOtherPdf));
        }

        public override void InitPath(IPathProcessor buffer)
        {
            base.InitPath(buffer);
            this.scene = pathIntegrator.Scene;
            this.Radiance = new RgbSpectrum(0f);
            this.Throughput = new RgbSpectrum(1f);
            this.PathState = PathTracerPathState.EyeVertex;
            if (this.secRays == null)
                this.secRays = new ShadowRayInfo[scene.ShadowRayCount];
            this.Sample = pathIntegrator.Sampler.GetSample(null);
            pathIntegrator.Scene.Camera.GetRay(Sample.imageX, Sample.imageY, out this.PathRay);
            this.RayIndex = -1;
            this.pathWeight = 1.0f;
            this.lastPdfW = 1.0f;
            this.tracedShadowRayCount = 0;
            this.depth = 0;
            this.specularBounce = true;
            if (ls == null)
                ls = new LightSample();
        }

        public override bool FillRayBuffer(RayBuffer rayBuffer)
        {
            var leftSpace = rayBuffer.LeftSpace();
            if (((PathState == PathTracerPathState.EyeVertex) && (1 > leftSpace)) ||
            ((PathState == PathTracerPathState.ShadowRaysOnly) && (tracedShadowRayCount > leftSpace)) ||
            ((PathState == PathTracerPathState.NextVertex) && (tracedShadowRayCount + 1 > leftSpace)))
                return false;
            if (PathState != PathTracerPathState.ShadowRaysOnly)
                RayIndex = rayBuffer.AddRay(ref PathRay);
            if (PathState == PathTracerPathState.NextVertex || PathState == PathTracerPathState.ShadowRaysOnly)
            {
                for (int i = 0; i < tracedShadowRayCount; ++i)
                    secRays[i].currentShadowRayIndex = rayBuffer.AddRay(ref secRays[i].shadowRay);
            }
            return true;
        }



        public override void Advance(RayBuffer rayBuffer, SampleBuffer consumer)
        {
            int currentTriangleIndex = 0;
#if VERBOSE

            try
            {
#endif
                var misWeight = 1.0f;

                base.Advance(rayBuffer, consumer);

                RayHit rayHit = rayBuffer.rayHits[RayIndex];

                if (((PathState == PathTracerPathState.ShadowRaysOnly) || (PathState == PathTracerPathState.NextVertex)) &&
                    (tracedShadowRayCount > 0))
                {
                    for (int i = 0; i < tracedShadowRayCount; ++i)
                    {
                        RayHit shadowRayHit = rayBuffer.rayHits[secRays[i].currentShadowRayIndex];
                        RgbSpectrumInfo attenuation;
                        if (this.ShadowRayTest(ref shadowRayHit, out attenuation))
                        {
                            //                            Radiance.MADD()
                            Radiance += (RgbSpectrum)(attenuation * ((secRays[i].color)));
                            pathWeight *= secRays[i].pdf;
                        }
                    }
                    tracedShadowRayCount = 0;
                    if (PathState == PathTracerPathState.ShadowRaysOnly)
                    {
                        Splat(consumer);
                        return;
                    }
                }

                depth++;
                bool missed = rayHit.Index == 0xffffffffu;
                Vector wo = -PathRay.Dir;

                if (missed || PathState == PathTracerPathState.ShadowRaysOnly || depth > scene.MaxPathDepth)
                {
                    if (depth > 0 && !specularBounce)
                    {
                        misWeight = Mis2(lastPdfW, 1f / MathLab.INVPI);
                    }

                    if (specularBounce)
                    {
                        Radiance += this.SampleEnvironment(wo) * Throughput * misWeight;
                    }

                    Splat(consumer);



                    return;
                }

                // Something was hit
                if (hitInfo == null)
                {
                    hitInfo = SurfaceSampler.GetIntersection(ref PathRay, ref rayHit);
                }
                else
                {
                    SurfaceSampler.GetIntersection(ref PathRay, ref rayHit, ref hitInfo);
                }
                currentTriangleIndex = (int)rayHit.Index;

                if (hitInfo == null)
                {
                    Debugger.Break();
                }
                //If Hit light)
                if (hitInfo.IsLight)
                {
                    var lt = scene.GetLightByIndex(currentTriangleIndex);

                    if (lt !=null)
                   {
                        float wpdf;
                        var l =(RgbSpectrum)lt.Emittance(ref wo, out wpdf)*Throughput;
                        if (!specularBounce & depth > 0)
                        {
                            var dpdf = MathLab.PdfAtoW(wpdf, rayHit.Distance, Vector.AbsDot(ref wo, ref hitInfo.Normal));
                            var lightPickProb = 1.0f/scene.Lights.Length;
                            misWeight = Mis2(lastPdfW, dpdf*lightPickProb);
                        }

                        Radiance += Throughput*l*misWeight;
                    }
                    else
                    {
                        float pdf;
                        var le = hitInfo.Color * lt.Emittance(ref wo, out pdf);
                        //Radiance += Throughput * le;

                        Radiance.MAdd(ref Throughput, ref le);
                    }
                    Splat(consumer);

                    return;
                }

                var hitPoint = PathRay.Point(rayHit.Distance);

                tracedShadowRayCount = 0;
                var bsdf = hitInfo.MMaterial;
                float contProb = bsdf.ContinuationProbability;

                if (bsdf.IsDiffuse())
                {
                    float lightStrategyPdf = scene.ShadowRayCount / (float)scene.Lights.Length;
                    RgbSpectrum lightTroughtput = Throughput * hitInfo.Color;
                    for (int i = 0; i < scene.ShadowRayCount; ++i)
                    {
                        int currentLightIndex = scene.SampleLights(Sample.GetLazyValue());
                        var light = scene.Lights[currentLightIndex];
                        int tries = RejectionSamplingMaxLighTries;
                    light_restart:
                        light.EvaluateShadow(ref hitPoint, ref hitInfo.ShadingNormal, Sample.GetLazyValue(), Sample.GetLazyValue(), Sample.GetLazyValue(), ref ls);
                        if (ls.Pdf <= 0f)
                        {
                            tries--;
                            if (tries == 0)
                                continue;
                            goto light_restart;
                        }
                        RgbSpectrum f_s;
                        bsdf.f(ref secRays[tracedShadowRayCount].shadowRay.Dir, ref wo, ref hitInfo.ShadingNormal, ref Throughput, out f_s);

                        if (f_s.IsBlack())
                        {
                            continue;
                        }
                        var fs = (RgbSpectrumInfo) f_s;
                        var bsdfPdfW = bsdf.Pdf(ref secRays[tracedShadowRayCount].shadowRay.Dir, ref wo);

                        float weight = 1f;


                        secRays[tracedShadowRayCount].color = (RgbSpectrumInfo)(RgbSpectrum)(ls.Spectrum);
                        secRays[tracedShadowRayCount].pdf = ls.Pdf;
                        secRays[tracedShadowRayCount].shadowRay = ls.LightRay;
                        Vector lwi = -secRays[tracedShadowRayCount].shadowRay.Dir;
                        var lightPdf = ls.Pdf * lightStrategyPdf;

                        if (!light.IsDelta)
                        {
                            bsdfPdfW *= contProb;
                            weight = Mis2(lightPdf, bsdfPdfW);
                            secRays[tracedShadowRayCount].pdf = 1f;
                            secRays[tracedShadowRayCount].color = (weight * Vector.AbsDot(ref hitInfo.ShadingNormal, ref lwi) / (lightPdf)) * (lightTroughtput * fs);
                        }
                        else
                        {
                            secRays[tracedShadowRayCount].color *= (lightTroughtput *
                                                                   Vector.AbsDot(ref hitInfo.ShadingNormal, ref lwi) *
                                                                   fs);
                            secRays[tracedShadowRayCount].color /= lightPdf;
                        }
                        if (!secRays[tracedShadowRayCount].color.IsBlack())
                        {
                            tracedShadowRayCount++;
                        }
                    }
                }

                float fPdf;
                Vector wi;
                int btries = RejectionSamplingMaxBsdfTries;

            bsdf_restart:
                RgbSpectrum f = bsdf.Sample_f(ref wo, out wi, ref hitInfo.Normal, ref hitInfo.ShadingNormal, ref Throughput,
                                                          Sample.GetLazyValue(), Sample.GetLazyValue(),
                                                          Sample.GetLazyValue(), ref hitInfo.TextureData,
                                                          out fPdf, out specularBounce)*hitInfo.Color;

                if ((fPdf <= 0.0f) || f.IsBlack())
                {
                    if (tracedShadowRayCount > 0)
                        PathState = PathTracerPathState.ShadowRaysOnly;
                    else
                    {
                        btries--;
                        if (btries == 0)
                        {
                            Splat(consumer);

                        }
                        else
                        {
                            goto bsdf_restart;
                        }
                    }
                    return;
                }
                pathWeight *= fPdf;
                Throughput *= (f*hitInfo.Color) / fPdf;
                lastPdfW = fPdf * contProb;
                if (depth > scene.MaxPathDepth)
                {
                    float prob = Math.Max(contProb, scene.RussianRuletteImportanceCap);
                    if (prob >= Sample.GetLazyValue())
                    {
                        Throughput /= prob;
                        pathWeight *= prob;
                    }
                    else
                    {
                        if (tracedShadowRayCount > 0)
                            PathState = PathTracerPathState.ShadowRaysOnly;
                        else
                        {
                            Splat(consumer);

                        }

                        return;
                    }
                }

                PathRay.Org = hitPoint;
                PathRay.Dir = wi.Normalize();
                PathState = PathTracerPathState.NextVertex;

#if VERBOSE
            }
            catch (Exception ex)
            {
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine("Advance path exception");
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine("Error triangle {0}", currentTriangleIndex);
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine(ex.Message);
                RayDen.Library.Components.SystemComponents.Tracer.TraceLine(ex.StackTrace);

            }
#endif
        }


        private bool ShadowRayTest(ref RayHit shadowRayHit, out RgbSpectrumInfo attenuation)
        {
            var hit = shadowRayHit.Index == 0xffffffffu || scene.IsLight((int)shadowRayHit.Index);
            attenuation = new RgbSpectrumInfo(1f);
            return hit;
            
            /*if (hit) return true;
            var mesh = scene.GetMeshByTriangleIndex((int)shadowRayHit.Index);
            var mat =
                //SurfaceMaterials.CreateMaterial(scene.MaterialProvider.Get(mesh.MaterialName));
                scene.MatLib.GetSurfMat(mesh != null ? mesh.MaterialName : "default");

            if (mat.AlphaTexture != null)
            {
                attenuation += (mat.AlphaTexture as ConstTextureInfo).Color;
                hit = true;
                //mat.MaterialData.Kt.Filter() < Sample.GetLazyValue();
            }

            return hit;*/
        }

        private RgbSpectrum SampleEnvironment(Vector vector)
        {
            return this.scene.SampleEnvironment(vector);
        }
    }
    // ReSharper restore RedundantThisQualifier
}